﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;

namespace Borg
{
    public static class AssemblyAnalyzerImpl
    {
        private static void AddAttribute(Dictionary<Type, IList<Attribute>> attributes, Attribute attr)
        {
            for (var type = attr.GetType(); typeof(Attribute).IsAssignableFrom(type); type = type.BaseType)
            {
                IList<Attribute> attrs;

                if (!attributes.TryGetValue(type, out attrs))
                {
                    attrs = new List<Attribute>();
                    attributes.Add(type, attrs);
                }

                attrs.Add(attr);
            }
        }

        public static AssemblyAnalyzerResult? Analyze(Uri originUri, Assembly assembly)
        {
            if (SkipAssembly(assembly))
                return null;

            var result = new AssemblyAnalyzerResult(originUri, assembly);
            var contract = InitContract(assembly, result);
            var plugin = InitPlugin(assembly, result);
            
            return
                contract || plugin
                ? new AssemblyAnalyzerResult?(result)
                : null;
        }

        private static T DeserializeAttribute<T>(CustomAttributeData cad)
        {
            var types = new Type[cad.ConstructorArguments.Count];
            var parameters = new object[cad.ConstructorArguments.Count];
            
            for (var i = 0; i < parameters.Length; i++)
            {
                var arg = cad.ConstructorArguments[i];
                types[i] = Type.GetType(arg.ArgumentType.AssemblyQualifiedName);
                parameters[i] = GetValue(arg);
            }

            var type = Type.GetType(cad.Constructor.DeclaringType.AssemblyQualifiedName);
            var local = (T)type.GetConstructor(types).Invoke(parameters);
            
            foreach (var namedArg in cad.NamedArguments)
            {
                switch (namedArg.MemberInfo.MemberType)
                {
                    case MemberTypes.Field:
                        type.GetField(namedArg.MemberInfo.Name).
                            SetValue(local, GetValue(namedArg.TypedValue));
                        break;

                    case MemberTypes.Property:
                        type.GetProperty(namedArg.MemberInfo.Name).
                            SetValue(local, GetValue(namedArg.TypedValue), null);
                        break;
                }
            }

            return local;
        }

        public static IDictionary<Type, IList<Attribute>> GetAllCustomAttributes(Type type)
        {
            var attributes = new Dictionary<Type, IList<Attribute>>();

            if (type.Assembly.ReflectionOnly)
            {
                foreach (var data in CustomAttributeData.GetCustomAttributes(type))
                    AddAttribute(attributes, DeserializeAttribute<Attribute>(data));

                return attributes;
            }

            foreach (var attribute in Attribute.GetCustomAttributes(type))
                AddAttribute(attributes, attribute);

            return attributes;
        }

        public static List<T> GetCustomAttributes<T>(Type type) where T : Attribute
        {
            IList<Attribute> attributes;
            var allCustomAttributes = GetAllCustomAttributes(type);
            var list = new List<T>();

            if (allCustomAttributes.TryGetValue(typeof (T), out attributes))
                list.AddRange(allCustomAttributes[typeof (T)].Cast<T>());

            return list;
        }

        private static object GetValue(CustomAttributeTypedArgument arg)
        {
            var argVal = arg.Value;

            if (argVal == null)
                return argVal;

            var c = arg.Value.GetType();
            var argumentType = arg.ArgumentType;

            if (argumentType.IsAssignableFrom(c))
                return argVal;

            var type = Type.GetType(argumentType.AssemblyQualifiedName);

            if (argumentType.IsArray)
            {
                if (argumentType.GetArrayRank() != 1)
                    throw new ArrayTypeMismatchException("Only one-dimensional arrays are supported.");

                var list = arg.Value as IList;

                if (list != null)
                {
                    var count = list.Count;
                    var val = type.GetConstructor(new[] { typeof(int) }).Invoke(new object[] { count });
                    var vals = val as IList;

                    for (var i = 0; i < count; i++)
                        if (vals != null)
                            vals[i] = GetValue((CustomAttributeTypedArgument) list[i]);

                    return val;
                }
            }

            if (!argumentType.IsEnum)
                throw new Exception("Unable to obtain value of " + arg);

            var argValue = (int)arg.Value;
            return Enum.Parse(type, argValue.ToString(CultureInfo.InvariantCulture));
        }

        public static bool HasAssemblyAttribute(Assembly assembly, Type attributeType)
        {
            return CustomAttributeData.GetCustomAttributes(assembly).
                Any(a => (attributeType.AssemblyQualifiedName ==
                    a.Constructor.DeclaringType.AssemblyQualifiedName));
        }

        private static bool HasCustomAttribute<T>(MemberInfo type)
        {
            return
                CustomAttributeData.GetCustomAttributes(type).
                Any(data => data.Constructor.DeclaringType.AssemblyQualifiedName ==
                    typeof (T).AssemblyQualifiedName);
        }

        private static bool InitContract(Assembly assembly, AssemblyAnalyzerResult result)
        {
            var initialized = false;

            foreach (var type in assembly.GetTypes())
            {
                var customAttributes = GetCustomAttributes<SlotDefinitionAttribute>(type);

                if (customAttributes.Count == 0)
                    continue;

                initialized = true;

                foreach (var attribute in customAttributes)
                    InitSlotDefinition(attribute, type, result);
            }

            return initialized;
        }

        private static void InitExtension(ExtensionAttribute extension, Type type, AssemblyAnalyzerResult result)
        {
            result.AddExtension(extension, type);

            foreach (var slot in GetCustomAttributes<SlotAttribute>(type))
                result.AddSlot(extension, slot);

            foreach (var plug in GetCustomAttributes<PlugAttribute>(type))
                result.AddPlug(extension, plug);

            foreach (var parameter in GetCustomAttributes<ParamAttribute>(type))
                result.AddParam(extension, parameter);
        }

        private static bool InitPlugin(Assembly assembly, AssemblyAnalyzerResult result)
        {
            var initialized = false;

            foreach (var type in assembly.GetTypes())
            {
                var customAttributes = GetCustomAttributes<ExtensionAttribute>(type);

                if (customAttributes.Count == 0)
                    continue;

                initialized = true;

                foreach (var attribute in customAttributes)
                    InitExtension(attribute, type, result);
            }

            return initialized;
        }

        private static void InitSlotDefinition(SlotDefinitionAttribute slotDef, Type type, AssemblyAnalyzerResult result)
        {
            result.AddSlotDefinition(slotDef, type);

            foreach (var tagDef in GetCustomAttributes<TagDefinitionAttribute>(type))
                result.AddTagDefinition(slotDef, tagDef);

            foreach (var paramDef in GetCustomAttributes<ParamDefinitionAttribute>(type))
                result.AddParamDefinition(slotDef, paramDef);
        }

        private static bool InternalIsAssemblyContract(Assembly assembly)
        {
            return assembly.GetTypes().Any(HasCustomAttribute<SlotDefinitionAttribute>);
        }

        private static bool InternalIsAssemblyPlugin(Assembly assembly)
        {
            return assembly.GetTypes().Any(HasCustomAttribute<ExtensionAttribute>);
        }

        public static bool IsAssemblyContract(Assembly assembly)
        {
            if (assembly == null)
                throw new ArgumentNullException("assembly");

            return InternalIsAssemblyContract(assembly);
        }

        public static bool IsAssemblyPlugin(Assembly assembly)
        {
            if (assembly == null)
                throw new ArgumentNullException("assembly");

            return InternalIsAssemblyPlugin(assembly);
        }

        private static bool SkipAssembly(Assembly assembly)
        {
            return HasAssemblyAttribute(assembly, typeof(NoAssemblyAnalyzerAttribute));
        }
    }
}
